<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
    <meta charset="utf-8">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <title>WLED自定义调色板编辑器</title>
    <script type="text/javascript">
      var d = document;
      function gId(e) {return d.getElementById(e);}
      function cE(e) {return d.createElement(e);}
    </script>
    
    <style>
      body {
        font-family: Arial, sans-serif;
        background-color: #111;
        font-size: 16px;
        color: #ddd;
        margin: 0 10px;
        line-height: 0.5;
      }
      #parent-container {
        position: relative;
        width: 100%;
        height: 20px;
      }
      #bottomContainer {
        position: absolute;
        margin-top: 50px;
      }
      #gradient-box {
        width: 100%;
        height: 100%;
      }
      .color-marker, .color-picker-marker {
        position: absolute;
        border-radius: 3px;
        background-color: rgb(192, 192, 192);
        border: 2px solid rgba(68, 68, 68, 0.5);
        z-index: 2;
      }
      .color-marker {
        height: 30px;
        width: 7px;
        top: 50%;
        transform: translateY(-50%);
      }
      .color-picker-marker {
        height: 7px;
        width: 7px;
        top: 150%;
      }
      .delete-marker {
        position: absolute;
        height: 5px;
        width: 5px;
        border-radius: 3px;
        background-color: rgb(255, 255, 255);
        border: 3px solid rgb(155, 40, 40);
        top: 220%;
        z-index: 2;
      }
      .color-picker {
        position: absolute;
        height: 1px;
        width: 1px;
        border: 1px;
        top: 150%;
        z-index: 1;
        border-color: #111;
        background-color: #111;
      }
      .buttonclass {
        padding: 0;
        margin: 0;
        vertical-align: bottom;
        background-color: #111;
      }
      #bottomContainer span {
        display: inline-flex;
        align-items: center;
        color: #fff; 
        font-size: 12px;
        vertical-align: middle;
      }
      #info {
        display: "";
        text-align: center;
        color: #fff; 
        font-size: 14px;
        position: relative;
        margin-top: 10px;
        line-height: 1;
      }
      .wrap {
        width: 800px;
        margin: 0 auto;
      }
      .palette {
        height: 20px;
      }
      .paletteGradients {
        flex: 1;
        height: 20px;
        border-radius: 3px;
      }
      .palettesMain {
        margin-top: 50px;
        width: 100%;
      }
      .palTop {
        height: fit-content;
        text-align: center;
        color: #fff; 
        font-size: 14px;
        position: relative;
        line-height: 1;
      }
      .palGradientParent {
        display: flex;
        align-items: center; 
        height: fit-content;
        margin-top: 10px;
        text-align: center;
        color: #fff; 
        font-size: 12px;
        position: relative;
        line-height: 1;
      }
      .buttonsDiv {
        display: inline-flex;
        margin-left: 5px;
        width: 50px;
      }
      .sendSpan, .editSpan{
        cursor: pointer;
      }
    </style>
  </head>
<body>
<div id="wrap" class="wrap">
  <div style="display: flex; justify-content: center;">
    <h1 style="display: flex; align-items: center;">
      <svg style="width:36px;height:36px;margin-right:6px;" viewBox="0 0 32 32">
        <rect style="fill:#003FFF" x="6" y="22" width="8" height="4"/>
        <rect style="fill:#003FFF" x="14" y="14" width="4" height="8"/>
        <rect style="fill:#003FFF" x="18" y="10" width="4" height="8"/>
        <rect style="fill:#003FFF" x="22" y="6" width="8" height="4"/>
      </svg>
      <span id="head">WLED自定义调色板编辑器</span>
    </h1>
  </div>

  <div id="parent-container">
    <div id="gradient-box"></div>
  </div>
  <div style="display: flex; justify-content: center;">
    <div id="palettes" class="palettesMain">
        <div id="palTop" class="palTop">
          当前正在使用的自定义调色板
        </div>
    </div>
  </div>

  <div style="display: flex; justify-content: center;">
    <div id="info">
      点击渐变编辑器添加新的颜色滑块，然后在滑块下面的彩色框更改其颜色。 
      点击指示器下方的红框(并确认)以进行删除。 
      完成后，单击箭头图标上传到所需的插槽。
      要编辑现有调色板，请单击铅笔图标。
    </div>
  </div>
  <div style="display: flex; justify-content: center;">
    <div id="staticPalettes" class="palettesMain">
        <div id="statpalTop" class="palTop">
          可用的静态调色板
        </div>
    </div>
  </div>  

</body>

<script type="text/javascript">
  //global variables
  var gradientBox = gId('gradient-box');
  var cpalc = -1;
  var pxCol = {};
  var tCol = {};
  var rect = gradientBox.getBoundingClientRect();
  var gradientLength = rect.width;
  var mOffs = Math.round((gradientLength / 256) / 2) - 5;
  var paletteArray = []; //Holds the palettes after load.
  var svgSave = '<svg style="width:25px;height:25px" viewBox="0 0 24 24"><path fill=#fff d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M7,12L12,17V14H16V10H12V7L7,12Z"/></svg>'
  var svgEdit = '<svg style="width:25px;height:25px" viewBox="0 0 24 24"><path fill=#fff d="M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M15.1,7.07C15.24,7.07 15.38,7.12 15.5,7.23L16.77,8.5C17,8.72 17,9.07 16.77,9.28L15.77,10.28L13.72,8.23L14.72,7.23C14.82,7.12 14.96,7.07 15.1,7.07M13.13,8.81L15.19,10.87L9.13,16.93H7.07V14.87L13.13,8.81Z"/></svg>'

  function recOf() {
    rect = gradientBox.getBoundingClientRect();
    gradientLength = rect.width;
    mOffs = Math.round((gradientLength / 256) / 2) - 5;
  }

  //Initiation
  getInfo();
  window.addEventListener('load', chkW);
  window.addEventListener('resize', chkW);

  gradientBox.addEventListener("click", clikOnGradient);
  
  //Sets start and stop, mandatory
  addC(0);
  addC(255);

  updateGradient(); //Sets the gradient at startup

  function clikOnGradient(e) {
    removeTrashcan(e);
    addC(Math.round((e.offsetX/gradientLength)*256));
  }

  ///////// Add a new colorMarker
  function addC(truePos, thisColor = '') {
    let position = -1;
    let iExist = false;
    const colorMarkers = gradientBox.querySelectorAll('.color-marker');

    colorMarkers.forEach((colorMarker, i) => {
      if (colorMarker.getAttribute("data-truepos") == truePos) {
        iExist = true;
      } 
    });

    if (colorMarkers.length > 17) iExist = true;
    if (iExist) return; // Exit the function early if iExist is true
    
    if (truePos > 0 && truePos < 255) {
      //calculate first available > 0
      for (var i = 1; i <= 16 && position < 1; i++) {
        if (!gId("colorMarker"+i)) {
          position = i;
        }
      }
    } else{
      position = truePos;
    }
    if (thisColor == ''){
      thisColor = `#${(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0')}`;// set random color as default
    }
      
    const colorMarker = cE('span'); // create a marker for the color position
    colorMarker.className = 'color-marker';
    colorMarker.id = 'colorMarker' + position.toString();
    colorMarker.setAttribute("data-truepos", truePos); //Used to always have a true position no matter what screen or percentage we use
    colorMarker.setAttribute("data-truecol", thisColor); //Used to hold the color of the position in the gradient connected to a true position
    colorMarker.setAttribute("data-offset", mOffs);
    colorMarker.addEventListener('click', stopFurtherProp); //Added to prevent the gradient click to fire when covered by a marker
    colorMarker.style.left = `${Math.round((gradientLength / 256) * truePos)+mOffs}px`;
    
    const colorPicker = cE('input');
    colorPicker.type = 'color';
    colorPicker.value = thisColor;
    colorPicker.className = 'color-picker';
    colorPicker.id = 'colorPicker' + position.toString();
    colorPicker.addEventListener('input', updateGradient);
    colorPicker.addEventListener('click',cpClk)

    const colorPickerMarker = cE('span'); // create a marker for the color position
    colorPickerMarker.className = 'color-picker-marker';
    colorPickerMarker.id = 'colorPickerMarker' + position.toString();
    colorPickerMarker.addEventListener('click', colClk);
    colorPickerMarker.style.left = colorMarker.style.left;
    colorPicker.style.left = colorMarker.style.left;
    
    const deleteMarker = cE('span'); // create a delete marker for the color position
    if (position > 0 && position < 255) {
      deleteMarker.className = 'delete-marker';
      deleteMarker.id = 'deleteMarker' + position.toString();
      deleteMarker.addEventListener('click', (e) => {
        deleteColor(e);
      });
      deleteMarker.style.left = colorMarker.style.left
    }

    colorMarker.style.backgroundColor = colorPicker.value; // set marker color to match color picker
    colorPickerMarker.style.backgroundColor = colorPicker.value;

    gradientBox.appendChild(colorPicker); 
    gradientBox.appendChild(colorMarker);
    gradientBox.appendChild(colorPickerMarker); 
    if (position != 0 && position != 255) gradientBox.appendChild(deleteMarker); // append the marker if not start or end
    //make markers slidable IF they are not the first or last slider
    if (position > 0 && position < 255) makeMeDrag(gId(colorMarker.id));

    setTooltipMarker(gId(colorMarker.id));

    updateGradient();
  }

  ///////// Update Gradient
  function updateGradient() {
    const colorMarkers = gradientBox.querySelectorAll('.color-marker');
    pxCol = {};
    tCol = {}
    colorMarkers.forEach((colorMarker, index) => {
      const thisColorPicker = gId(colorMarkers[index].id.replace('colorMarker', 'colorPicker'));
      const colorToSet = thisColorPicker.value;
      gId(colorMarkers[index].id.replace('colorMarker', 'colorPickerMarker')).style.backgroundColor = colorToSet;
      colorMarkers[index].style.backgroundColor = colorToSet;
      colorMarkers[index].setAttribute("data-truecol", colorToSet);
      const tPos = colorMarkers[index].getAttribute("data-truepos");
      const gradientPos = Math.round((gradientLength / 256)*tPos);
      pxCol[gradientPos] = colorToSet;
      tCol[tPos] = colorToSet;
    });
    gradientString = 'linear-gradient(to right';
    Object.entries(pxCol).forEach(([p, c]) => {
      gradientString += `, ${c} ${p}px`;
    });
    gradientString += ')';
    gradientBox.style.background = gradientString;
    //gId("jsonstring").innerHTML = calcJSON();
  }

  function stopFurtherProp(e) {
    e.stopPropagation();
  }

  function colClk(e){
    removeTrashcan(e)
    e.stopPropagation();
    let cp = gId(e.srcElement.id.replace("Marker",""));
    cp.click();
  }

  function cpClk(e) {
    removeTrashcan(event)
    e.stopPropagation();
  }

  //This neat little function makes any element draggable on the X-axis.
  //Just call: makeMeDrag(myElement); And you are good to go.
  function makeMeDrag(elmnt) {
    var posNew = 0, mousePos = 0, mouseOffset = 0
    
    //Set these to whatever you want to limit your movement to
    var rect = gradientBox.getBoundingClientRect();
    var maxX = rect.right; // maximum X coordinate
    var minX = rect.left; // minimum X coordinate i.e. also offset from left of screen
    var gradientLength = maxX - minX + 1;

    elmnt.onmousedown = dragMouseDown;

    function dragMouseDown(e) {
      removeTrashcan(event)
      e = e || window.event;
      e.preventDefault();
      // get the mouse cursor position at startup:
      mousePos = e.clientX;
      d.onmouseup = closeDragElement;
      // call a function whenever the cursor moves:
      d.onmousemove = elementDrag;
    }

    function elementDrag(e) {
      e = e || window.event;
      e.preventDefault();
      // calculate the new cursor position:
      posNew = mousePos - e.clientX;
      mousePos = e.clientX;
      mousePosInGradient = mousePos - (minX + 1)
      
      truePos = Math.round((mousePosInGradient/gradientLength)*256);
      oldTruePos = elmnt.getAttribute("data-truepos");
      // set the element's new position if new position within min/max limits:
      if (truePos > 0 && truePos < 255 && oldTruePos != truePos) {
        if (truePos < 64) {
          thisOffset = 0;
        } else if (truePos > 192) {
          thisOffset = 7;
        } else {
          thisOffset=3;
        }
        elmnt.style.left = (Math.round((gradientLength/256)*truePos)+mOffs) + "px";
        gId(elmnt.id.replace('colorMarker', 'colorPickerMarker')).style.left = elmnt.style.left;
        gId(elmnt.id.replace('colorMarker', 'deleteMarker')).style.left = elmnt.style.left;
        gId(elmnt.id.replace('colorMarker', 'colorPicker')).style.left = elmnt.style.left;
        elmnt.setAttribute("data-truepos", truePos);
        setTooltipMarker(elmnt);
        updateGradient();
      }
    }

    function closeDragElement() {
      /* stop moving when mouse button is released:*/
      d.onmouseup = null;
      d.onmousemove = null;
    }
  }

  function setTooltipMarker(elmnt) {
    elmnt.setAttribute('title', `${elmnt.getAttribute("data-truepos")} : ${elmnt.getAttribute("data-truecol")}`)
  }

  function deleteColor(e) {
    var trash = cE("div");
    thisDeleteMarker = e.srcElement;
    thisColorMarker = gId(thisDeleteMarker.id.replace("delete", "color"));
    thisColorPickerMarker = gId(thisDeleteMarker.id.replace("delete", "colorPicker"));
    thisColorPicker = gId(thisDeleteMarker.id.replace("deleteMarker", "colorPicker"));
    renderOffsetX = 15 - 5;
    renderX = e.srcElement.getBoundingClientRect().x - renderOffsetX;
    renderY = e.srcElement.getBoundingClientRect().y + 13;
    
    trash.id = "trash";
    trash.innerHTML = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30px" height="30px"><path style="fill:#880000; stroke: #888888; stroke-width: -2px;stroke-dasharray: 0.1, 8;" d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z"/></svg>';
    trash.style.position = "absolute";
    trash.style.left = (renderX) + "px";
    trash.style.top = (renderY) + "px";
    d.body.appendChild(trash);
    
    trash.addEventListener("click", (e)=>{
      trash.parentNode.removeChild(trash);
      thisDeleteMarker.parentNode.removeChild(thisDeleteMarker);
      thisColorPickerMarker.parentNode.removeChild(thisColorPickerMarker);
      thisColorMarker.parentNode.removeChild(thisColorMarker);
      thisColorPicker.parentNode.removeChild(thisColorPicker);
      updateGradient();
    });
    e.stopPropagation();
    // Add event listener to close the trashcan on outside click
    d.addEventListener("click", removeTrashcan);
    e.stopPropagation();
  }

  function removeTrashcan(event) {
    trash = gId("trash");
    if (event.target != trash && trash) {
      trash.parentNode.removeChild(trash);
      d.removeEventListener("click", removeTrashcan);
    }
  }

  function chkW() {
    //Possibly add more code that recalculates the gradient... Massive job ;)
    const wrap = gId('wrap');
    const head = gId('head');
    if (wrap.offsetWidth < 600) {
      head.style.display = 'none';
    } else {
      head.style.display = 'inline';
    }
  }

  function calcJSON() {
    let rStr = '{"palette":['
    Object.entries(tCol).forEach(([p, c]) => {
      if (p > 0) rStr += ',';
      rStr += `${p},"${c.slice(1)}"`; // store in hex notation
      //rStr += `${p},${parseInt(c.slice(1, 3), 16)},${parseInt(c.slice(3, 5), 16)},${parseInt(c.slice(5, 7), 16)}`;
    });
    rStr += ']}';
    return rStr;
  }

  function initiateUpload(idx) {
    const data = calcJSON();
    const fileName = `/palette${idx}.json`;
    uploadJSON(data, fileName);
  }

  function uploadJSON(jsonString, fileName) {
    //Some indication on "I'm working"
    var req = new XMLHttpRequest();
    var blob = new Blob([jsonString], {type: "application/json"});
    req.addEventListener('load', ()=>{
      console.log(this.responseText, ' - ',  this.status)
      localStorage.removeItem('wledPalx');
      //setTimeout(()=>{
      //  ss.setAttribute('fill', '#fff');
      //}, 1000);
      //setTimeout(()=>{window.location.href='/';},2000);
      window.location.href = '/'; //Guessing we want to return ASAP when we get confirmation save is done
    });
    req.addEventListener('error', (e)=>{
      console.log('Error: ', e); console.log(' Status: ', this.status);
      //Show some error notification for some time
      setTimeout(()=>{
        //Remove it when time has pased
      }, 1000);
    });
    req.open("POST", "/upload");
    var formData = new FormData();
    formData.append("data", blob, fileName);
    req.send(formData);
    return false;
  }

  async function getInfo() {
    hst = location.host;
    if (hst.length > 0 ) {
      try {
        var arr = [];
        const response = await fetch('http://'+hst+'/json/info');
        const json = await response.json();
        cpalc = json.cpalcount;
        fetchPalettes(cpalc-1);
      } catch (error) {
        console.error(error);
      }
    } else {
      console.error('cannot identify host');
    }
  }

  async function fetchPalettes(lastPal) {
    paletteArray.length = 0;
    for (let i = 0; i <= lastPal; i++) {
      const url = `http://${hst}/palette${i}.json`;
      try {
        const response = await fetch(url);
        const json = await response.json();
        paletteArray.push(json);
      } catch (error) {
        console.error(`Error fetching JSON from ${url}: `, error);
      }
    }
    //If there is room for more custom palettes, add an empty, gray slot
    if (paletteArray.length < 10) {
      //Room for one more :)
      paletteArray.push({"palette":[0,70,70,70,255,70,70,70]});
    }

    //Get static palettes from localStorage and do some magic to reformat them into the same format as the pallete JSONs
    //This code excludes any objects with "non valid integer colors", i.e. r, c1, c2, c3 and such
    //This code also fixes potentially broken palettes which doesn't end on 255
    //The code finally also removes any representations of the custom palettes, since we read them from file

    const wledPalx = JSON.parse(localStorage.getItem('wledPalx'));
    if (!wledPalx) {
      alert("您的浏览器缺少调色板的缓存。在返回这里之前，您可能应该返回到主页面，让它正确加载，以便调色板缓存重新生成。","缺少缓存的调色板！")
    } else {
      for (const key in wledPalx.p) {
        if (key > 245) {
          delete wledPalx.p[key];
          continue;
        }
        const arr = wledPalx.p[key];
        let valid = true;
        for (const subArr of arr) {
          if (!Array.isArray(subArr) || subArr.length !== 4) {
            valid = false;
            break;
          }
          for (const val of subArr) {
            if (typeof val !== 'number' || val < 0 || val > 255 || !Number.isInteger(val)) {
              valid = false;
              break;
            }
          }
        }
        if (!valid) {
          delete wledPalx.p[key];
          continue;
        }
        const lastArr = arr[arr.length - 1];
        if (lastArr[0] !== 255) {
          const copyArr = [...lastArr];
          copyArr[0] = 255;
          arr.push(copyArr);
        }
      }

      const pArray = Object.entries(wledPalx.p).map(([key, value]) => ({
        [key]: value.flat()
      }));

      paletteArray.push( ...pArray);
    }
    generatePaletteDivs();
  }

  function generatePaletteDivs() {
    const palettesDiv = d.getElementById("palettes");
    const staticPalettesDiv = d.getElementById("staticPalettes");
    const paletteDivs = Array.from(palettesDiv.children).filter((child) => {
      return child.id.match(/^palette\d$/); // match only elements with id starting with "palette" followed by a single digit
    });

    for (const div of paletteDivs) {
      palettesDiv.removeChild(div); // remove each div that matches the above selector
    }

    for (let i = 0; i < paletteArray.length; i++) {
      const palette = paletteArray[i];
      const paletteDiv = d.createElement("div");
      paletteDiv.id = `palette${i}`;
      paletteDiv.classList.add("palette");
      const thisKey = Object.keys(palette)[0];
      paletteDiv.dataset.colarray = JSON.stringify(palette[thisKey]);

      const gradientDiv = d.createElement("div");
      gradientDiv.id = `paletteGradient${i}`
      const buttonsDiv = d.createElement("div");
      buttonsDiv.id = `buttonsDiv${i}`;
      buttonsDiv.classList.add("buttonsDiv")

      const sendSpan = d.createElement("span");
      sendSpan.id = `sendSpan${i}`;
      sendSpan.onclick = function() {initiateUpload(i)};
      sendSpan.setAttribute('title', `将当前编辑器发送到插槽${i}`); // perhaps Save instead of Send?
      sendSpan.innerHTML = svgSave;
      sendSpan.classList.add("sendSpan")
      const editSpan = d.createElement("span");
      editSpan.id = `editSpan${i}`;
      editSpan.onclick = function() {loadForEdit(i)};
      editSpan.setAttribute('title', `复制插槽${i}的调色板到编辑器`);
      editSpan.innerHTML = svgEdit;
      editSpan.classList.add("editSpan")

      gradientDiv.classList.add("paletteGradients");
      let gradientColors = "";

      for (let j = 0; j < palette[thisKey].length; j += 2) {
        const position = palette[thisKey][j];
        if (typeof(palette[thisKey][j+1]) === "string") {
          gradientColors += `#${palette[thisKey][j+1]} ${position/255*100}%, `;
        } else {
          const red = palette[thisKey][j + 1];
          const green = palette[thisKey][j + 2];
          const blue = palette[thisKey][j + 3];
          gradientColors += `rgba(${red}, ${green}, ${blue}, 1) ${position/255*100}%, `;
          j += 2;
        }
      }

      gradientColors = gradientColors.slice(0, -2); // remove the last comma and space
      gradientDiv.style.backgroundImage = `linear-gradient(to right, ${gradientColors})`;
      paletteDiv.className = "palGradientParent";
      if (thisKey == "palette") {
        buttonsDiv.appendChild(sendSpan); //Only offer to send to custom palettes
      } else{
        editSpan.style.marginLeft = "25px";
      }
      if (i!=cpalc) {
        buttonsDiv.appendChild(editSpan); //Dont offer to edit the empty spot
      }
      paletteDiv.appendChild(gradientDiv);
      paletteDiv.appendChild(buttonsDiv);
      if (thisKey == "palette") {
        palettesDiv.appendChild(paletteDiv);
      } else {
        staticPalettesDiv.appendChild(paletteDiv);
      }
    }
  }

  function loadForEdit(i) {
    d.querySelectorAll('input[id^="colorPicker"]').forEach((input) => {
      input.parentNode.removeChild(input);
    });
    d.querySelectorAll('span[id^="colorMarker"], span[id^="colorPickerMarker"], span[id^="deleteMarker"]').forEach((span) => {
      span.parentNode.removeChild(span);
    });
    
    let colArray = JSON.parse(gId(`palette${i}`).getAttribute("data-colarray"));
    
    for (let j = 0; j < colArray.length; j += 2) {
      const position = colArray[j];
      let hex;
      if (typeof(colArray[j+1]) === "string") {
        hex = `#${colArray[j+1]}`;
      } else {
        const red = colArray[j + 1];
        const green = colArray[j + 2];
        const blue = colArray[j + 3];
        hex = rgbToHex(red, green, blue);
        j += 2;
      }
      addC(position, hex);
      window.scroll(0, 0);
    }
  }

  function rgbToHex(r, g, b) {
    const hex = ((r << 16) | (g << 8) | b).toString(16);
    return "#" + "0".repeat(6 - hex.length) + hex;
  }
</script>
</html>
